package it.unicam.cs.cosy.bioshape.core.components;

import it.unicam.cs.cosy.bioshape.core.behaviours.Behaviour;
import it.unicam.cs.cosy.bioshape.core.components.movers.EntityDriver;
import it.unicam.cs.cosy.bioshape.core.components.movers.LinearMotionDriver;
import it.unicam.cs.cosy.bioshape.core.util.Parameters;
import it.unicam.cs.cosy.bioshape.core.util.XmlReader;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

import javax.vecmath.Point3d;
import javax.vecmath.Vector3d;

import vclip.PolyTree;
import vclip.Vertex;

/**
 * Contains all the data necessary to the placement and the creation of
 * {@link Entity} objects.
 * 
 * @author Gaston Alanis, Matteo Micheletti, Andrea Piermarteri, Federico Buti
 */
public class EntityData implements ProtoEntity {

    /** auto-generated serial ID */
    private static final long serialVersionUID = 425199495584500006L;

    private Point3d boundingBox = new Point3d();

    private double boundingVolume;

    private int cardinality;

    private Vector3d[] centers;

    int indexCenters;

    private double mass;

    private String name;

    private Vector3d[] orientations;

    // default entity class
    private String entityClass = "it.unicam.cs.cosy.bioshape.core.components.ShapedEntity";

    private String boundsType;

    // default value is rectilinear driver, indeed <driver> tag is optional!
    // private EntityDriver driver = new LinearMotionDriver(new Double(1),
    // new Double(1));

    private double percep;

    // Information to create an instance of the Driver assigned in the input
    // file
    private Parameters parametersOfInputDriver;

    // Information of where to find the class file of the Driver assigned in the
    // input file
    private String pathOfClassFileForInputDriver;

    // Library of polytrees to create personalized polytrees
    private Map<String, PolyTree> library;

    /**
     * An {@link EntityData} with a vertexes ArrayList and an orientation.
     * 
     * @param name
     *            The PolyTree name
     * @param library 
     *            The library of {@code PolyTree}s 
     * @param mass
     *            Mass of the final {@code Entity} object
     * @param b
     *            Shape Calculus behaviour associated to the entity
     * @param percep
     *            Perception radius of the entity
     * @param cardinality
     *            cardinality of the instances to create
     */
    public EntityData(String name, Map<String,PolyTree> library, double mass, Behaviour b,
            double percep, int cardinality) {
        this.name = name;
        this.mass = mass;
        this.percep = percep;
        this.library = library;
        // TODO behaviour b Luca: O_O
        setUp();
        this.cardinality = cardinality;
        centers = new Vector3d[cardinality];
        orientations = new Vector3d[cardinality];
    }

//    /**
//     * An {@link EntityData} with a vertexes ArrayList and an orientation.
//     * 
//     * @param name
//     *            The PolyTree name
//     * @param poly
//     *            The associated {@code PolyTree} object
//     * @param cardinality
//     *            cardinality of the instances to create
//     */
//    public EntityData(String name, PolyTree poly, int cardinality) {
//        this.name = name;
//        this.poly = poly;
//        setUp();
//        this.cardinality = cardinality;
//        centers = new Vector3d[cardinality];
//        orientations = new Vector3d[cardinality];
//    }

//    /**
//     * An {@link EntityData} with a vertexes ArrayList and an orientation.
//     * 
//     * @param libs
//     *            the PolyTree input files
//     * @param name
//     *            the name of the entity
//     * @param cardinality
//     *            cardinality of the instances to create
//     * @throws IOException
//     *             if the passed file is invalid
//     */
//    public EntityData(String name, String[] libs, int cardinality)
//            throws IOException {
//        this.name = name;
//        this.setUp(libs);
//        this.cardinality = cardinality;
//        centers = new Vector3d[cardinality];
//        orientations = new Vector3d[cardinality];
//    }

    /**
     * Computes the three dimensional points needed to generate the Bounding
     * Box.
     */
    private void calculateBounding(ArrayList<Point3d> vertexList) {
        double minx = 0, miny = 0, minz = 0;
        double maxx = 0, maxy = 0, maxz = 0;
        for (Point3d pOS : vertexList) {
            if (pOS.getX() < minx)
                minx = pOS.getX();
            else if (pOS.getX() > maxx)
                maxx = pOS.getX();
            if (pOS.getY() < miny)
                miny = pOS.getY();
            else if (pOS.getY() > maxy)
                maxy = pOS.getY();
            if (pOS.getZ() < minz)
                minz = pOS.getZ();
            else if (pOS.getZ() > maxz)
                maxz = pOS.getZ();
        }
        boundingBox.setX(maxx - minx);
        boundingBox.setY(maxy - miny);
        boundingBox.setZ(maxz - minz);
    }

    @Override
    public Point3d getBB() {
        return boundingBox;
    }

    @Override
    public double getBBVolume() {
        return boundingVolume;
    }

    @Override
    public int getCardinality() {
        return cardinality;
    }

    // /**
    // * Computes this {@link EntityData} BoundingBox centre: it will be
    // replaced
    // * afterwards with the true centre, assigned during the placement.
    // *
    // * @return the BoundingBox centre
    // */
    // public Point3d getBoundingBoxCentre() {
    // double minx = 0, miny = 0, minz = 0;
    // double maxx = 0, maxy = 0, maxz = 0;
    // for (Point3d pOS : vertexList) {
    // if (pOS.getX() < minx)
    // minx = pOS.getX();
    // else if (pOS.getX() > maxx)
    // maxx = pOS.getX();
    // if (pOS.getY() < miny)
    // miny = pOS.getY();
    // else if (pOS.getY() > maxy)
    // maxy = pOS.getY();
    // if (pOS.getZ() < minz)
    // minz = pOS.getZ();
    // else if (pOS.getZ() > maxz)
    // maxz = pOS.getZ();
    // }
    // return new Point3d((maxx + minx) / 2, (maxy + miny) / 2,
    // (maxz + minz) / 2);
    // }

    @Override
    public Vector3d[] getCenters() {
        return centers;
    }

    @Override
    public double getMass() {
        return mass;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public Vector3d[] getOrientations() {
        return orientations;
    }

    /**
     * Calculates the entity Bounding Box volume.
     */
    private void setBBVolume() {
        this.boundingVolume = boundingBox.getX() * boundingBox.getY()
                * boundingBox.getZ();
    }

    @Override
    public void addCenterAndRotation(Vector3d center, Vector3d orientation) {
        centers[indexCenters] = center;
        if (orientation != null) {
            orientation.x = Math.toRadians(orientation.x);
            orientation.y = Math.toRadians(orientation.y);
            orientation.z = Math.toRadians(orientation.z);
        }
        orientations[indexCenters++] = orientation;
    }

    /**
     * sets up the {@code EntityData} for the placement algorithm.
     */
    public void setUp() {
        ArrayList<Point3d> vertexList = new ArrayList<Point3d>();
        Vertex[] verts = library.get(this.name).getPolyhedron().getVerts();
        for (int i = 0; i < verts.length; i++) {
            vertexList.add(new Point3d(verts[i].getCoords().getX(), verts[i]
                    .getCoords().getY(), verts[i].getCoords().getZ()));
        }
        calculateBounding(vertexList);
        setBBVolume();
    }

//    /**
//     * sets up the {@code EntityData} for the placement algorithm.
//     * 
//     * @param library
//     *            array of filenames from which load {@code PolyTree} objects
//     * 
//     * @throws IOException
//     *             if something goes wrong with the reading of the
//     *             {@code PolyTree}
//     */
//    public void setUp(String[] library) throws IOException {
//        ArrayList<Point3d> vertexList = new ArrayList<Point3d>();
//        HashMap<Object, Object> lib = new HashMap<Object, Object>();
//        for (int i = 0; i < library.length; i++) {
//            PolyTree.scanLibrary(library[i], lib, true);
//        }
//        poly = new PolyTree(name, lib, name);
//        Vertex[] verts = poly.getPolyhedron().getVerts();
//        for (int i = 0; i < verts.length; i++) {
//            vertexList.add(new Point3d(verts[i].getCoords().getX(), verts[i]
//                    .getCoords().getY(), verts[i].getCoords().getZ()));
//        }
//        calculateBounding(vertexList);
//        setBBVolume();
//    }

    @Override
    public Vector3d getCenter(int index) {
        return centers[index];
    }

    @Override
    public Vector3d getOrientation(int index) {
        return orientations[index];
    }

    @Override
    public void setEntityClass(String classString) {
        entityClass = classString;
    }

    @Override
    public void setBoundsType(String classString) {
        boundsType = classString;
    }

    @Override
    public void setInputDriverInformation(Parameters parameters,
            String pathOfClassFile) {
        this.parametersOfInputDriver = parameters;
        this.pathOfClassFileForInputDriver = pathOfClassFile;
    }

    @Override
    public double getradius() {
        return library.get(this.name).radius();
    }

    @Override
    public Entity generateEntity(long id, Vector3d center) {
        ClassLoader classLoader = XmlReader.class.getClassLoader();
        try {
            Class<?> c = Class.forName(entityClass, false, classLoader);
            // parameters
            Class<?>[] classes = new Class[] { long.class, double.class,
                    PolyTree.class, String.class, double.class, Vector3d.class,
                    EntityDriver.class };
            // Luca: creo una copia diversa del polytree per ogni entità creata,
            // uso un nome diverso
            PolyTree personalizedPolyTree = library.get(this.name);
            personalizedPolyTree.setName(this.name + "#" + id);
            // Luca per sicurezza Creo tutti i convex hull
            // personalizedPolyTree.buildAllBoundingHulls(PolyTree.CONVEX_HULL);
            // Luca una istanza personalizzata del driver
            EntityDriver personalizedDriver = (EntityDriver) getInstanceOfClassFromParameters(
                    this.parametersOfInputDriver,
                    this.pathOfClassFileForInputDriver);
            Object[] objects = new Object[] { new Long(id), new Double(mass),
                    personalizedPolyTree, boundsType, new Double(percep),
                    center, personalizedDriver };
            return (Entity) c.getConstructor(classes).newInstance(objects);
        } catch (SecurityException e) {
            e.printStackTrace();
            return null;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private Object getInstanceOfClassFromParameters(Parameters parameters,
            String path) throws IllegalArgumentException {
        ClassLoader classLoader = XmlReader.class.getClassLoader();
        try {
            // TODO maybe can be powered-up with Reflections library for
            // non-fully qualified name search
            Class<?> classOfTheInstance = Class.forName(
                    path + parameters.getName(), false, classLoader);
            if (parameters.getParameter() == null)
                return classOfTheInstance.getConstructor().newInstance();
            // parameters
            Class<?>[] classes = new Class[parameters.getParameter().size()];
            Object[] objects = new Object[parameters.getParameter().size()];
            for (int i = 0; i < parameters.getParameter().size(); i++) {
                classes[i] = Class.forName(parameters.getParameter().get(i)
                        .getFormalparameter());
                objects[i] = checkType(classes[i], parameters.getParameter()
                        .get(i).getActualparameter());
            }
            return classOfTheInstance.getConstructor(classes).newInstance(
                    objects);
        } catch (SecurityException e) {
            if (e.getClass().getSimpleName().contains("ClassNotFound"))
                System.out.println("The class '" + parameters.getName()
                        + "' was not found. Please "
                        + "check your input for errors in path.");
            System.exit(0);
        } catch (Exception e) {
            if (e.getClass().getSimpleName().contains("ClassNotFound"))
                System.out.println("The class '" + parameters.getName()
                        + "' was not found. Please "
                        + "check your input for errors in path.");
            System.exit(0);
        }
        return null;
    }

    private Object checkType(Class<?> clazz, String actualparameter) {
        if (clazz.getCanonicalName().contains("Double"))
            return new Double(actualparameter);
        if (clazz.getCanonicalName().contains("Integer"))
            return new Integer(actualparameter);
        if (clazz.getCanonicalName().contains("Long"))
            return new Long(actualparameter);
        if (clazz.getCanonicalName().contains("Float"))
            return new Float(actualparameter);
        if (clazz.getCanonicalName().contains("Short"))
            return new Short(actualparameter);
        return actualparameter;
    }

    /*
     * (non-Javadoc)
     * 
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return "EntityData [boundingBox=" + boundingBox + ", boundingVolume="
                + boundingVolume + ",\n cardinality=" + cardinality
                + ", centers=" + Arrays.toString(centers) + ", indexCenters="
                + indexCenters + ",\n mass=" + mass + ", name=" + name
                + ", orientations=" + Arrays.toString(orientations) + ", PolyTree library="
                + library + ", entityClass=" + entityClass + ", boundsType="
                + boundsType + ", driverInfoParameters="
                + this.parametersOfInputDriver + ", percep=" + percep + "]";
    }

}